
from bson import SON
from app.errors import NotFound, ParameterError
from flask_redis import FlaskRedis
from flask import abort
from flask_mongoengine import MongoEngine, Pagination as _Pagination
from mongoengine.errors import DoesNotExist, ValidationError
from mongoengine.queryset import QuerySet
from pymongo import InsertOne as MetaInsertOne, UpdateOne as MetaUpdateOne

# mongo = PyMongo(connect=False)
db = MongoEngine()

redis = FlaskRedis()

MAX_PAGE = 2 ** 62


class Pagination(_Pagination):
    def __init__(self, iterable, page, per_page, not_err=False):

        if not_err:
            if page < 1:
                page = 1
            if page > MAX_PAGE:
                page = MAX_PAGE
            if per_page < 1:
                per_page = 1
            if per_page > MAX_PAGE:
                per_page = MAX_PAGE
        if page < 1 or per_page < 1 or page > MAX_PAGE or per_page > MAX_PAGE:
            abort(404)

        self.iterable = iterable
        self.page = page
        self.per_page = per_page

        if isinstance(iterable, QuerySet):
            self.total = iterable.count()
        else:
            self.total = len(iterable)

        start_index = (page - 1) * per_page
        end_index = page * per_page

        self.items = iterable[start_index:end_index]
        # if isinstance(self.items, QuerySet):
        #     self.items = self.items.select_related()
        if (not self.items) and (page != 1) and (not not_err):
            abort(404)

    def page_info(self):
        return {
            'currentPage': self.page,
            'hasNextPage': self.has_next,
            'hasPreviousPage': self.has_prev,
            'itemCount': self.total,
            'pageCount': self.pages,
            'perPage': self.per_page
        }


class CustomQuerySet(QuerySet):
    def find(self, _filter: dict = None) -> list:
        if not _filter:
            return self().all()
        else:
            return self(__raw__=_filter)

    def find_one(self, _filter: dict):
        results = self.find(_filter)
        if results:
            return results[0]

        return None

    def paginate(self, page, per_page, **kwargs):
        """
        Paginate the QuerySet with a certain number of docs per page
        and return docs for a given page.
        """
        return Pagination(self, page, per_page, not_err=kwargs.get("not_err", False))

    def pagenation(self, page, limit=10):
        total = len(self())
        total_page = ((total - 1) // limit) + 1
        if page > total_page:
            page = total_page
        if page < 1:
            page = 1

        return self().skip((page - 1) * limit).limit(limit), total, page, total_page

    def get_or_404(self, *args, **kwargs):
        """
        Get a document and raise a 404 Not Found error if it doesn't
        exist.
        """
        try:
            return self.get(*args, **kwargs)
        except DoesNotExist:
            if not (msg := kwargs.get("msg", None)):
                msg = f'{self._collection.name} not found'
            raise NotFound(msg=msg)

    def bulk_write(self, requests, **kwargs):
        self._collection.bulk_write(requests=requests, **kwargs)


class BaseDocument(db.Document):
    meta = {
        'abstract': True,
        'queryset_class': CustomQuerySet,
        'strict': False
    }

    def validate(self, clean=True):
        try:
            super(BaseDocument, self).validate()
        except ValidationError as e:
            raise ParameterError(msg=e.message)

    @classmethod
    def from_dict(cls, dict_data, created=False):
        return cls._from_son(SON(dict_data), created=created)


class BaseEmbeddedDocument(db.EmbeddedDocument):
    meta = {
        'abstract': True,
        'strict': False,
    }

    def validate(self, clean=True):
        try:
            super(BaseEmbeddedDocument, self).validate()
        except ValidationError as e:
            raise ParameterError(msg=e.message)

    def to_dict(self):
        return self.to_mongo().to_dict()

    @classmethod
    def from_dict(cls, dict_data, created=False):
        """Converts dict data to a Document instance

        :param str dict_data: The dict data to load into the Document
        :param bool created: Boolean defining whether to consider the newly
            instantiated document as brand new or as persisted already:
            * If True, consider the document as brand new, no matter what data
              it's loaded with (i.e. even if an ID is loaded).
            * If False and an ID is NOT provided, consider the document as
              brand new.
            * If False and an ID is provided, assume that the object has
              already been persisted (this has an impact on the subsequent
              call to .save()).
            * Defaults to ``False``.
        """
        return cls._from_son(SON(dict_data), created=created)


class InsertOne(MetaInsertOne):
    pass


class UpdateOne(MetaUpdateOne):
    pass
